JVM GC 调优与问题排查思路

序言

  在 Java 应用的运行过程中,JVM 会通过垃圾回收机制自动管理内存,减少人工手动管理内存的负担。但在实际业务场景中,随着对象分配量增加、长期存活对象累积以及业务负载变化,GC 可能逐渐成为影响应用性能的重要因素。

  例如,Young GC 频繁是否意味着年轻代空间不足?Full GC 之后老年代占用依然很高,又意味着什么?当应用出现 OOM 时,我们应该优先检查堆内存配置,还是排查对象引用关系?

  面对这些问题,单纯增加内存或者更换垃圾收集器,可能并不能从根本上解决问题。更合理的方式是先通过 GC 日志和 JVM 监控数据了解应用的运行状态,再结合对象分配速率、对象存活情况、晋升速率以及老年代增长趋势,逐步定位问题。

  本文将从 GC 调优需要关注的核心指标出发,介绍常见 GC 问题的分析思路,并结合 jstat、jcmd、jmap 等工具,演示如何获取和分析 JVM 运行数据,最终根据实际数据制定合理的调优方案。

GC 调优应该关注哪些指标?

  GC 调优并不是简单地调整 -Xms、-Xmx 或某个收集器参数,而是需要先判断当前 GC 是否已经影响业务,再定位造成问题的对象和内存区域。

  常见的关注指标包括:

指标 关注内容 主要用途
GC 频率 Young GC、Mixed GC、Full GC 发生次数 判断回收是否过于频繁
GC 停顿 单次停顿、最大停顿、P95/P99 判断业务线程受到多大影响
GC 总耗时 一段时间内 GC 消耗的时间 判断 GC 对系统运行的整体影响
对象分配速率 单位时间分配的对象大小 判断对象创建是否过于密集
对象晋升速率 单位时间晋升到老年代的对象大小 判断年轻代存活对象是否过多
FGC 后老年代占用 回收完成后的老年代存活对象规模 判断长期存活对象是否持续增长
CPU 使用率 业务线程和 GC 线程的 CPU 消耗 判断并发 GC 是否造成资源竞争
应用延迟 接口平均耗时、P95、P99 判断 GC 是否影响业务 SLO

  注意判断指标时,GC 次数多不一定意味着存在问题。比如一个应用每秒发生多次 Young GC,但每次只暂停几毫秒,GC 后能够回收大量临时对象,整体延迟仍然稳定,那么这种情况可能是正常的。

  相反,如果 Full GC 次数不多,但每次耗时数秒,就可能严重影响在线请求。因此,需要将 GC 频率、停顿时间、回收效果和业务延迟结合起来分析。

Young GC 频繁应该如何分析?

  Young GC 频繁通常意味着年轻代空间消耗得比较快,但它不一定是异常。首先需要判断对象是短生命周期还是长生命周期。

大量短生命周期对象

  如果应用频繁创建临时字符串、集合、DTO 或中间转换对象,但这些对象很快失去引用,那么 GC 可以在年轻代回收它们。

  比如说,一个应用持续创建大量临时对象,导致 Eden 空间不足,进而触发 Young GC,发现大部分对象已经不可达,最终对象被回收,年轻代获得空间。

  上述情况下,主要表现可能是 Young GC 频率较高。如果单次停顿短、回收效果好,并且业务延迟符合要求,就不一定需要调整。

大量长生命周期对象

  如果年轻代回收时有大量对象仍然存活,它们就需要复制到 Survivor 区,或者在满足相应条件时晋升到老年代。此时不仅 Young GC 可能变慢,还可能增加老年代晋升压力。

  比如说,一个应用持续创建对象,触发 Young GC 后发现大量对象仍然存活,此时它们会复制到 Survivor 或晋升老年代,如果老年代占用增加,可能触发更重的老年代回收。

  当前,年轻代空间不足,并不意味着所有对象都会进入老年代。不可达对象会直接被回收,只有仍然存活的对象才可能进入 Survivor 或晋升老年代。

疑问:Young GC 是否可以通过调大年轻代解决?

  适当增大年轻代可能扩大 Eden 空间,容纳更多短生命周期对象,从而降低 Young GC 发生频率。但它并不是无代价的。

  例如,在传统分代堆中,堆总量固定为 4GB:

  • 调整前整个堆 4GB
    • 年轻代 1GB
    • 老年代 3GB
  • 调整后整个堆 4GB
    • 年轻代 2GB
    • 老年代 2GB

  此时,年轻代变大后,可能降低回收频率,但老年代空间同时减少。如果对象存活率较高,单次 Young GC 需要处理的存活对象可能更多,暂停时间也可能增加;老年代还可能更早出现空间压力。

  因此,是否调整年轻代,需要结合对象分配速率、对象存活率、晋升速率、停顿时间和老年代空间综合判断。对于 G1 等区域化收集器,也不应直接套用固定年轻代、老年代比例的传统调优思路。

Full GC 频繁应该如何分析?

  Full GC 频繁通常需要重点关注,但不能只根据“发生 Full GC”就判断是内存泄漏。不同收集器对 Full GC 的触发条件和日志表现并不完全相同,需要结合 GC 日志判断。

  常见的分析方向包括:

  • 堆空间配置不合理:业务存活集确实较大,但堆内存上限不足
  • 对象晋升过快:年轻代存活对象多,持续进入老年代,导致老年代空间快速下降
  • 老年代长期存活对象过多:缓存、集合或静态引用持有大量对象,回收后占用仍然很高
  • 收集器回收跟不上分配速度:对象分配速率较高,GC 尚未完成时可用空间就已经不足
  • 大对象分配或巨型对象区域压力:某些大对象分配策略可能造成连续空间或 Region 资源压力

  其中一个很有价值的观察点是:Full GC 或老年代回收完成后,老年代占用量是否明显下降。 如果回收后占用仍然很高,并且随运行时间持续增长,就应该优先排查长期存活对象和内存泄漏,而不是直接调大年轻代。

OOM 与 GC 问题的关系

  OOM(OutOfMemoryError)并不等于对象创建频繁,也不一定是 Java 堆不足。不同内存区域都可能出现内存相关异常。

异常信息 常见排查方向
Java heap space 堆内存不足、存活对象过多、大对象分配或内存泄漏
GC overhead limit exceeded GC 长期占用大量时间,但回收效果很差
Metaspace 类元数据占用过多、动态生成类过多或元空间配置不足
Direct buffer memory NIO、Netty 等使用的直接内存不足
unable to create new native thread 线程数量过多或本地内存、系统线程资源不足
StackOverflowError 递归过深或线程栈空间不足,通常不是堆内存问题

  因此,生产环境出现 OOM 时,应该先确认异常类型,再选择对应的排查工具和方法。对于 Java 堆问题,可以分析 GC 日志、对象直方图和 Heap Dump;对于直接内存或线程创建失败,则需要进一步检查 Native Memory、线程数量及容器资源限制。

使用 GC 日志和工具定位问题

  GC 调优的基础是有足够的观测数据,不能仅凭某一次接口变慢或某个内存曲线就直接修改参数。

  对于这些数据,我们可以通过工具来处理,后续的文章会详细介绍,这里只做简要说明。

JDK 8 开启 GC 日志

1
2
3
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log

  这些参数可以记录 GC 时间、回收详情和 GC 日志文件,便于后续分析。

使用 jstat 观察 GC

1
jstat -gcutil <pid> 1000

  jstat -gcutil 可以按照指定时间间隔输出各个内存区域使用比例和 GC 次数等信息,适合快速观察 GC 是否频繁、老年代占用是否持续增长。

使用 jcmd 查看堆和 JVM 参数

1
2
3
jcmd <pid> VM.flags
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram

  其中:

  • VM.flags:查看 JVM 实际启动参数
  • GC.heap_info:查看堆内存基本信息
  • GC.class_histogram:查看对象类型及实例数量、占用情况,可用于初步发现异常对象

生成 Heap Dump 分析对象引用

1
jmap -dump:format=b,file=/data/dump/heap.hprof <pid>

  生成 Heap Dump 后,可以使用 MAT 等工具分析 Histogram、Dominator Tree 和 GC Roots 引用链,找出哪些对象占用了大量内存,以及它们为什么无法被回收。

  需要注意,Heap Dump 可能包含业务数据和敏感信息,生产环境生成、传输和分析时应做好访问控制和数据保护。

运行情况分析思路

  在通过 GC 日志、Arthas、JVM 监控平台等工具定位问题之后,可以进一步结合运行时数据,对 JVM 当前的内存使用情况进行量化分析,了解应用的对象分配、垃圾回收和对象晋升情况,为后续的 JVM 参数调整提供依据。

  例如,在调整堆内存和年轻代老年代大小之前,我们需要先了解:

  • 应用每秒大概创建多少对象?
  • Young GC 多久触发一次?
  • 每次 Young GC 后有多少对象存活?
  • Young GC 的存活对象中又有多少进入了老年代?
  • 老年代的增长速度如何?
  • Full GC 的频率和耗时是否已经影响业务?

  对于 JDK 8,可以使用 jstat 对 JVM 的内存区域和 GC 计数器进行采样,它适合快速观察运行趋势。由于其输出的是采样时刻的状态和累计计数,并不是完整的 GC 事件日志。因此,如果涉及单次 GC 的准确停顿时间、对象年龄分布和晋升详情时,需要结合 GC 日志进行具体分析。

获取 JVM 运行时数据

  jstat 是 JDK 自带的 JVM 统计监控工具,可以查看堆各区域的容量、使用量及 YGC、FGC 的次数和累计耗时。

  例如,执行下面的命令:

1
jstat -gc <pid> 1000 10

  其中:

  • <pid>:Java 进程的进程 ID
  • 1000:采样间隔,单位为毫秒,这里表示每隔 1 秒采样一次
  • 10:采样次数,这里表示总共输出 10 次

  -gc 会输出一组与堆内存和 GC 相关的数据。JDK 8 中比较常用的字段如下:

字段 含义 单位或说明
S0C / S1C Survivor 0 / Survivor 1 的容量 KB
S0U / S1U Survivor 0 / Survivor 1 的使用量 KB
EC Eden 区当前容量 KB
EU Eden 区当前使用量 KB
OC 老年代当前容量 KB
OU 老年代当前使用量 KB
YGC Young GC 累计次数 次
YGCT Young GC 累计耗时 秒
FGC Full GC 累计次数 次
FGCT Full GC 累计耗时 秒
GCT GC 累计总耗时 秒

  其中,EU、S0U、S1U 和 OU 表示采样时刻各内存区域的使用量;YGC、YGCT、FGC 和 FGCT 则是从 JVM 启动以来不断累加的计数器或耗时。

  分析时需要区分这两类数据:内存使用量是某个时刻的快照,可能随着 GC 发生而下降;GC 次数和累计耗时是累积值,通常只增不减。要计算某个观测窗口内的 GC 次数和平均耗时,应当使用两次采样结果的差值,而不是直接使用进程启动以来的累计值。

确认年轻代对象分配速率

  年轻代对象分配速率,主要用于估算应用在单位时间内向年轻代分配了多少对象。对于传统分代收集器,可以先观察 Eden 区的使用变化。

  执行下面的命令,每隔 1 秒采样一次:

1
jstat -gc <pid> 1000 60

  重点关注 EC 和 EU 两个字段:EC 表示 Eden 区容量,EU 表示当前已经使用的空间。

  如果两次采样之间没有发生 Young GC,那么 Eden 区使用量的增长可以近似反映这段时间内向 Eden 分配对象的速度。假设两次采样相隔 10 秒,EU 从 100MB 增长到 300MB,那么这段时间 Eden 占用量的净增长约为 20MB/s。

  可以用下面的公式进行粗略估算:

$$
R_{eden} \approx \frac{EU_2 - EU_1}{\Delta t}
$$

  其中,EU 使用相同单位,Δt 使用秒,计算结果就是单位时间内 Eden 使用量的近似增长速度。

  但这里有一个非常重要的限制:EU 是当前占用量,不是累计分配量。 当 Young GC 发生时,Eden 中的对象会被回收或转移,EU 通常会明显下降。因此,如果两次采样之间发生了 Young GC,就不能简单地用 EU 的前后差值计算对象分配速率,否则可能得到负数或严重偏低的结果。

  如果采样窗口内发生了 Young GC,可以结合 YGC 的变化,观察两次 Young GC 之间的时间间隔,并参考 GC 日志中 GC 前 Eden 的使用量,估算每轮 Eden 的消耗量和平均分配速率。仅用 Eden 容量除以 GC 间隔,也只能得到一个粗略估值,因为 Eden 不一定每次都恰好使用到容量上限,而且对象也可能通过其他路径分配。

  注意:采样间隔需要根据业务负载调整。例如,在请求量较高、Young GC 比较频繁的时段,可以使用 1 秒或更短的监控间隔;在系统负载较低、主要观察长期趋势时,可以适当放宽到 1 分钟甚至 10 分钟。但采样间隔越长,越容易错过 GC 前后的内存变化,因此不适合用长间隔分析单次 GC 的细节。

  另外,系统通常存在业务高峰期和日常低峰期。建议分别在不同时间段采样,避免只根据低峰期数据设置 JVM 参数,导致高峰期出现 Eden 快速耗尽、Young GC 频繁等问题。

确认 YGC 的触发频率和平均耗时

  了解年轻代的对象分配情况后,可以进一步分析 Young GC 的发生频率和平均耗时。

  jstat -gc 中的 YGC 表示 Young GC 的累计次数,YGCT 表示 Young GC 的累计耗时。分别记录观测窗口开始和结束时的数值,这样就可以计算这段时间内发生了多少次 Young GC,以及这些 GC 一共消耗了多少时间。

  假设观测窗口为 10 分钟,开始时 YGC=100、YGCT=2.5,结束时 YGC=160、YGCT=5.5,那么:

  • Young GC 次数:ΔYGC = 160 - 100 = 60
  • Young GC 累计耗时:ΔYGCT = 5.5 - 2.5 = 3 秒
  • 平均每次 Young GC 耗时:3 / 60 = 0.05 秒,即 50ms
  • 平均触发间隔:600 / 60 = 10 秒

  对应的计算公式为:

$$
T_{YGC_avg} = \frac{\Delta YGCT}{\Delta YGC}
$$

$$
T_{YGC_interval} = \frac{\Delta t}{\Delta YGC}
$$

  如果 ΔYGC 为 0,说明该采样窗口内没有观察到 Young GC,此时不能计算单次 Young GC 的平均耗时。

  通过这些数据,我们可以初步判断应用多久会发生一次 Young GC,以及 GC 对业务线程造成的平均停顿大概有多长。

  注意:平均值无法反映最大停顿和长尾情况。例如,大部分 Young GC 只有几毫秒,但偶尔一次达到数百毫秒,平均值可能掩盖这种问题。因此,如果需要分析单次停顿、最大停顿或 P99 停顿时间,需要以 GC 日志或监控平台记录的事件明细为准。

确认每次 Young GC 后的对象存活量和晋升量

  Young GC 发生时,年轻代中的对象并不会全部进入老年代。不可达对象会被回收,仍然存活的对象则可能进入 Survivor 区,也可能因为年龄达到晋升条件、Survivor 空间不足或收集器自身的分配策略而进入老年代。

  因此,在分析 Young GC 时,除了关注 Eden 区使用量是否下降,还需要观察 Survivor 区和老年代使用量的变化。

  可以继续使用下面的命令进行采样:

1
jstat -gc <pid> 1000 120

  当发现 YGC 计数增加时,结合相邻采样点观察以下字段:

  • EU:Young GC 后 Eden 区的使用量通常会明显下降
  • S0U / S1U:两个 Survivor 区会交替承担 From 和 To 的角色,观察实际承载存活对象的 Survivor 区使用量
  • OU:观察老年代使用量是否增加,以及增加幅度

  对于传统分代收集器,如果能够在 Young GC 前后及时采样,并且确认这段时间没有发生老年代回收,那么老年代使用量的增加可以作为对象晋升量的近似参考。例如,某次 Young GC 前后 OU 增加了 80MB,可以初步认为这段时间老年代净增加了约 80MB 的对象。

  但需要注意,ΔOU 并不一定全部来自 Young GC 晋升。某些对象可能直接在老年代分配;同时,如果期间发生了 CMS 并发回收、Full GC 或其他老年代回收,OU 还可能下降。因此,仅通过两个采样点的老年代使用量差值,无法精确还原本次 Young GC 的晋升字节数。 想要获得更准确的晋升信息,应结合 GC 日志,必要时开启对象年龄分布输出。

  在 JDK 8 中,可以在启动参数中增加:

1
2
3
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution
-Xloggc:/path/to/gc.log

  其中,PrintTenuringDistribution 可以帮助观察 Survivor 区中不同年龄对象的分布,以及当前收集器采用的对象年龄阈值。通过这些数据,可以进一步判断对象是否因为 Survivor 空间不足或年龄阈值较低而提前晋升。

  如果发现每次 Young GC 后都有大量对象进入老年代,就需要继续分析这些对象是否确实需要长期存活。如果它们只是业务处理中产生的短期对象,却因为 Survivor 空间不足等原因提前晋升,就可能增加老年代压力;如果它们本身就是缓存、会话或长期业务数据,那么晋升属于正常现象,应当重点评估老年代是否有足够空间容纳这些存活对象。

确认老年代增长速率与 Full GC 频率

  在了解对象分配、Young GC 和对象晋升情况后,就可以进一步分析老年代的增长趋势。

  jstat -gc 中的 OC 表示老年代容量,OU 表示老年代当前使用量。可以在相同业务负载下,定期记录 OU 的变化,估算老年代净增长速率:

$$
R_{old} \approx \frac{OU_2 - OU_1}{\Delta t}
$$

  为了减少误差,最好选择相同 GC 阶段进行比较,例如比较多次 Young GC 完成后的老年代使用量。同时,需要确认观测窗口内是否发生过 Full GC、CMS 并发回收或 G1 Mixed GC 等老年代回收事件。如果发生过回收,OU 的变化反映的是对象晋升、直接分配和老年代回收共同作用后的净结果,而不是单纯的晋升量。

  在老年代容量相对稳定、老年代使用量持续增长且没有明显回收的情况下,可以根据当前空闲空间和老年代净增长速率,粗略估算老年代距离空间压力还有多长时间:

$$
T_{pressure} \approx \frac{OC - OU}{R_{old}}
$$

  这个公式只能用于趋势预估,不能直接预测 Full GC 的准确触发时间。不同收集器的回收触发条件不同,例如 CMS 会根据并发回收启动阈值提前开始回收,G1 则根据堆占用和自身策略启动并发标记周期;因此,Full GC 不一定等到老年代使用量达到容量上限才发生。

  除了观察老年代使用量,还可以通过 FGC 和 FGCT 统计 Full GC 的发生次数和累计耗时。与 Young GC 的计算方式相同,应当使用观测窗口内的差值:

$$
T_{FGC_avg} = \frac{\Delta FGCT}{\Delta FGC}
$$

$$
T_{FGC_interval} = \frac{\Delta t}{\Delta FGC}
$$

  例如,在 30 分钟的观测窗口内,FGC 增加了 3 次,FGCT 增加了 6 秒,那么 Full GC 的平均耗时约为 2 秒,平均发生间隔约为 10 分钟。

  需要注意,FGCT 是累计耗时,FGCT / FGC 得到的是 JVM 启动以来的历史平均值;如果只想分析当前业务高峰期或某个具体观测窗口,就应该使用 ΔFGCT / ΔFGC。另外,如果观测窗口内 ΔFGC=0,则说明没有观察到 Full GC,不能计算本窗口内的单次平均耗时。

制定初始调优方案

  通过前面的分析,我们可以得到一组相对有价值的运行数据指标:

  • 年轻代对象分配速率
  • Young GC 触发频率
  • Young GC 平均耗时
  • 每次 Young GC 后的存活对象规模与对象晋升速率
  • 老年代增长速率
  • Full GC 的发生频率和耗时。

  有了这些数据,就可以结合业务目标制定初始 JVM 参数。例如,先根据机器可用内存和应用实际存活对象规模确定堆内存范围,再结合对象分配速率、Young GC 停顿和晋升压力,评估年轻代大小、Eden 与 Survivor 的比例、老年代可用空间以及对象晋升阈值是否合理。

  传统分代收集器中,常见的相关参数包括:

参数 作用 调整时需要关注
-Xms 堆初始大小 启动阶段的堆内存需求
-Xmx 堆最大大小 存活数据规模、容器或机器内存限制
-Xmn / -XX:NewRatio 控制年轻代大小或年轻代与老年代比例 Young GC 频率与老年代空间之间的平衡
-XX:SurvivorRatio 控制 Eden 与单个 Survivor 区的比例 Survivor 空间是否容易不足
-XX:MaxTenuringThreshold 设置对象晋升年龄阈值的最大值 对象年龄分布、Survivor 使用情况和晋升速率

  大对象的分配策略和阈值需要结合具体收集器判断,不能将某个参数理解为所有 GC 都通用的大对象晋升开关。例如,传统分代收集器与 G1 对大对象的处理方式并不完全相同;G1 中还需要关注 Humongous Region 的使用情况。

  此外,G1 会根据停顿目标和堆使用情况动态管理年轻代与老年代 Region。对于 G1,不宜直接照搬传统分代收集器固定年轻代比例的调优方式,更不应该在没有测量依据时通过固定年轻代大小限制其自适应能力。

  这里还需要纠正一个常见的调优误区:并不是要求每次 Young GC 后存活对象都必须小于 Survivor 空间的 50%,更不是要尽可能阻止所有对象进入老年代。 在传统 HotSpot 分代收集器中,TargetSurvivorRatio 等策略会参与动态年龄阈值的计算,但它不是所有收集器都必须满足的硬性存活比例。对象达到年龄阈值、Survivor 空间不足或符合收集器的大对象分配策略时,都可能晋升到老年代。

  真正需要关注的是是否发生了不合理的提前晋升,以及晋升速率是否超过老年代长期容纳和回收的能力。对于缓存、会话等确实需要长期存活的对象,进入老年代是正常现象;此时应该评估缓存容量、淘汰策略和老年代空间,而不是一味提高晋升年龄,强行让对象停留在 Survivor 区。

  因此,GC 调优并不是单纯追求 Young GC 越少越好、Full GC 越少越好,也不是让所有对象尽量留在年轻代。更合理的目标是:在满足业务延迟和吞吐量要求的前提下,让对象分配、年轻代回收、对象晋升和老年代回收形成相对稳定的平衡,避免不必要的提前晋升和频繁的长时间 Full GC。

小结

  通过 jstat,我们可以快速获取 JVM 堆内存各区域的使用情况,以及 Young GC、Full GC 的累计次数和耗时。结合多次采样的差值,可以估算对象分配趋势、GC 触发频率、平均 GC 耗时和老年代净增长情况。

  但需要明确,jstat 提供的是采样快照和累计计数,无法单独精确还原每一次 GC 前后的对象分布,也不能仅凭 EU 或 OU 的变化就断定对象分配量和晋升量。对于单次 GC 停顿、对象年龄分布、晋升详情以及 Full GC 的具体原因,还需要结合 GC 日志和其他分析工具。

  最终,JVM 参数应该建立在实际业务数据和性能目标之上。先通过监控了解对象分配与存活特征,再评估堆大小、年轻代比例、Survivor 空间和晋升策略,最后通过压测和线上观测验证调整效果,才是相对可靠的 GC 调优过程。

最佳实践

  在生产环境中,GC 调优不应该停留在修改几个 JVM 参数,而是需要结合 GC 日志、对象分配速率、对象晋升速率、GC 后老年代占用、CPU 使用率和接口延迟,判断问题究竟来自短生命周期对象分配过多、长期存活对象过多、缓存设计不合理,还是堆内存配置不足。

先分析对象生命周期,再决定是否调整参数

  GC 问题通常与对象分配速率、对象存活率和长期引用关系密切相关:

  • 出现 Young GC 频繁时,应先判断是否只是短生命周期对象分配较多
  • 出现老年代占用持续增长时,应检查对象是否被缓存、集合、静态变量或线程上下文长期引用
  • 如果对象确实不再需要,应从业务代码中释放引用
  • 如果是缓存,则应该设计容量、过期和淘汰策略;如果业务确实需要保留大量对象,再评估堆内存是否需要扩大

不要在没有数据的情况下盲目调参

  -Xms、-Xmx、年轻代大小、GC 线程数、停顿时间目标等参数都会影响收集器行为。调整前应当先确认当前收集器、堆使用情况、GC 日志和业务信息。

  建议每次只调整少量相关参数,并在相同负载、相同机器资源和相同运行时长下进行对比。否则,即使 GC 指标发生变化,也很难确定变化究竟来自哪个参数。

升级 JDK 时重新验证收集器配置

  JDK 升级可能带来默认收集器变化、GC 参数废弃、日志格式变化以及收集器实现优化。例如,JDK 8 的默认收集器与 JDK 9 之后的默认选择并不相同,CMS 也在 JDK 14 被移除。

  因此,从 JDK 8 升级到 JDK 17 或 JDK 21 时,不能直接照搬旧项目的 CMS 参数。应该检查启动参数是否仍然有效,选择目标版本支持的收集器,再通过压测和生产灰度验证性能表现。

文章信息

时间 说明
2026-08-03 初稿
0%